﻿#include "seqbase/store.h"
#include "seqbase/graphics.h"
#include "seqbase/sequence.h" // for serialization
#include "serializable.h"
#include "texture.h"

using namespace std;
using namespace anip;

constexpr int ArchiveMagic = 0x70696e61; // 61 6e 69 70 'anip'

class DataStore::SerializeHelper
{
public:
	static void serializeObjectBase(const Entity& obj, ostream& out);
	static void deserializeObjectBase(Entity& obj, istream& in);
	static unsigned int fileVersion;
};

void DataStore::SerializeHelper::serializeObjectBase(const Entity& obj, ostream& out)
{
	binaryWrite(obj._id, out);
	binaryWrite(obj.name, out);
	binaryWrite(obj._parent->id(), out);
	binaryWrite(obj.tags.size(), out);
	for (auto& t : obj.tags)
		binaryWrite(t, out);
}

void DataStore::SerializeHelper::deserializeObjectBase(Entity& obj, istream& in)
{
	// assume id has already been read
	binaryRead(obj.name, in);
	int pid = binaryRead<int>(in);
	if (pid >= 0)
	{
		obj._parent = &getOrCreateFromId<Directory>(pid);
		obj._parent->_entities.insert({ obj._id, &obj });
	}
	else obj._parent = nullptr;
	size_t tagSize = binaryRead<size_t>(in);
	for (; tagSize > 0; tagSize--)
		obj.tags.insert(binaryRead<wstring>(in));
}


int DataStore::_maxId = 1;
set<int> DataStore::_usableIds;
map<int, DataStore::Entity*> DataStore::_allEntities;
DataStore::Directory DataStore::root(0);

unsigned int DataStore::SerializeHelper::fileVersion;

int DataStore::newId()
{
	if (_usableIds.size() > 0)
	{
		int result = *_usableIds.cbegin();
		_usableIds.erase(result);
		return result;
	}
	return _maxId++;
}

// allow getting root for directories only
// ANIP_TODO: reduce the redundant code in the two functions?
template<>
DataStore::Directory& DataStore::getOrCreateFromId(int id)
{
	if (id == 0) return root;
	assert(id > 0);
	auto f = _allEntities.find(id);
	if (f == _allEntities.end())
	{
		auto obj = new Directory(id);
		_allEntities.insert(make_pair(id, obj));
		return *obj;
	}
	return dynamic_cast<Directory&>(*f->second);
}

#undef DEF_getOrCreateFromId

void DataStore::serialize(ostream& out)
{
	binaryWrite(ArchiveMagic, out);
	binaryWrite(anip::version, out);
	binaryWrite(_allEntities.size(), out);
	for (auto i : _allEntities)
		anip::serialize(*i.second, out);
}

void DataStore::deserialize(istream& in)
{
	if (binaryRead<int>(in) != ArchiveMagic)
		throw runtime_error("Invalid archive file");
	SerializeHelper::fileVersion = binaryRead<unsigned int>(in);
	int size = binaryRead<size_t>(in);
	for (int i = 0; i < size; i++)
		anip::deserialize<Entity>(in);
}

#pragma region Serialization

serializer_implementation(DataStore::TextureObject)(Entity& e, ostream& out)
{
	auto& obj = (TextureObject&)e;
	SerializeHelper::serializeObjectBase(obj, out);

	Texture2D& tex = *obj.pointer();
	binaryWrite(obj.isExternal, out);
	if (obj.isExternal)
	{
		binaryWrite(obj.path, out);
		binaryWrite(tex.format(), out);
	}
	else
	{
		binaryWrite(tex.width(), out);
		binaryWrite(tex.height(), out);
		binaryWrite(tex.format(), out);
		size_t len;
		unsigned char* data = tex.getData(tex.format(), len);
		binaryWrite(len, out);
		out.write((char*)data, len);
		delete[] data;
	}
}

deserializer_implementation(DataStore::TextureObject)(istream& in)
{
	auto& obj = getOrCreateFromId<STexture2D>(binaryRead<int>(in));
	SerializeHelper::deserializeObjectBase(obj, in);

	binaryRead(obj.isExternal, in);
	if (obj.isExternal)
	{
		TextureOptions opt;
		binaryRead(obj.path, in);
		opt.format = binaryRead<ColorFormat>(in);
		obj._pointer = Texture2D::createFromFile(ws2utf8s(obj.path), &opt);
	}
	else
	{
		int w = binaryRead<int>(in);
		int h = binaryRead<int>(in);
		TextureOptions opt;
		opt.format = binaryRead<ColorFormat>(in);
		auto len = binaryRead<size_t>(in);
		auto data = new unsigned char[len];
		in.read((char*)data, len);
		obj._pointer = Texture2D::createFromData(w, h, data, &opt);
		delete[] data;
	}
	return &obj;
}

serializer_implementation(DataStore::SequenceObject)(Entity& e, ostream& out)
{
	auto& obj = (SequenceObject&)e;
	SerializeHelper::serializeObjectBase(obj, out);
	anip::serialize(*obj.pointer(), out);
}

deserializer_implementation(DataStore::SequenceObject)(istream& in)
{
	auto& obj = getOrCreateFromId<SSequence>(binaryRead<int>(in));
	SerializeHelper::deserializeObjectBase(obj, in);
	obj._pointer = anip::deserialize<Sequence>(in);
	return &obj;
}

serializer_implementation(DataStore::FrameGroupObject)(Entity& e, ostream& out)
{
	auto& obj = (FrameGroupObject&)e;
	SerializeHelper::serializeObjectBase(obj, out);
	anip::serialize(*obj.pointer(), out);
}

deserializer_implementation(DataStore::FrameGroupObject)(istream& in)
{
	auto& obj = getOrCreateFromId<SFrameGroup>(binaryRead<int>(in));
	SerializeHelper::deserializeObjectBase(obj, in);
	obj._pointer = anip::deserialize<FrameGroup>(in);
	return &obj;
}

serializer_implementation(DataStore::Directory)(Entity& e, ostream& out)
{
	auto& obj = (Directory&)e;
	SerializeHelper::serializeObjectBase(obj, out);
	// needless to record anything more since objects and subs deserialize to proper directories
}

deserializer_implementation(DataStore::Directory)(istream& in)
{
	auto& obj = getOrCreateFromId<Directory>(binaryRead<int>(in));
	SerializeHelper::deserializeObjectBase(obj, in);

	return &obj;
}

#pragma endregion

void DataStore::Entity::moveTo(Directory* dir)
{
	if (_parent == dir) return;
	if (_parent != nullptr)
		_parent->remove(_id);
	if (dir != nullptr)
		dir->add(*this);
}

#pragma region Directory

#define DEF_newXXX(name1, name2, name3)								\
DataStore::name2& DataStore::Directory::name1(name3* tex, int id)	\
{																	\
	if (id < 0) id = newId();										\
	auto obj = new name2(id, this, tex);							\
	_entities.insert({ obj->_id, obj });							\
	_allEntities.insert({ obj->_id, obj });							\
	return *obj;													\
}

DEF_newXXX(newTexture, TextureObject, Texture2D)
DEF_newXXX(newSequence, SequenceObject, Sequence)
DEF_newXXX(newFrameGroup, FrameGroupObject, FrameGroup)

DataStore::Directory& DataStore::Directory::newDirectory(const wstring& name, int id)
{
	if (id < 0) id = newId();
	Directory* obj = new Directory(id);
	obj->_parent = this;
	obj->name = name;
	_entities.insert({ id, obj });
	_allEntities.insert({ id, obj });
	return *obj;
}

#undef DEF_newXXX

#pragma endregion
